﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace SuperBall3D
{
    public class Geometry
    {
        GeometryType geomType;
        float wScale, hScale, dScale, rScale;
        Vector3 position;
        Vector3 oldPosition;
        public enum GeometryType : byte
        {
            Sphere,
            Box,
            MaxCode
        };

        public Geometry(GeometryType t, Model thisMod, Vector3 center, float widthScale, float heightScale, float depthScale)
        {
            if (t != GeometryType.Box)
                throw new Exception("6 Arg Geometry constructor for boxes only");
            
            wScale = widthScale;
            hScale = heightScale;
            dScale = depthScale;
            position = center;
            bBox = makeBoundingBoxFromModel(thisMod, Matrix.CreateTranslation(center));
            geomType = t;
        }

        public Geometry(GeometryType t, Model thisMod, Vector3 center, float radiusScale)
        {

            if (t != GeometryType.Sphere)
                throw new Exception("4 Arg Geometry constructor for spheres only");

            rScale = radiusScale;
            bSphere = makeBoundingSphereFromModel(thisMod);
            position = center;
            bSphere.Center = center;
            geomType = t;

        }

        public GeometryType GeomType
        {
            get{ return geomType;}
        }


        const byte maxType = (byte)GeometryType.MaxCode;

        BoundingBox bBox;

        BoundingSphere bSphere;


        public BoundingBox BBox
        {
            get
            {
                if (geomType == GeometryType.Box)
                    return bBox;
                else throw new Exception("Trying to get BoundingBox of a non-box");
            }
        }
        
        public BoundingSphere BSphere
        {
            get
            {
                if (geomType == GeometryType.Sphere)
                    return bSphere;
                else throw new Exception("Trying to get BoundingSphere of a non-sphere");
            }
        }


        public void UpdateGeometry(Vector3 newPos)
        {
            oldPosition = position;
            position = newPos;
            if (position == oldPosition)
                return; 
            if (this.GeomType == GeometryType.Box)
                UpdateBox(position - oldPosition);
            else if (this.GeomType == GeometryType.Sphere)
                UpdateSphere(position);

        }
        void UpdateBox(Vector3 deltaCenterPos)
        {
            bBox.Min = bBox.Min + deltaCenterPos;
            bBox.Max = bBox.Max + deltaCenterPos;
        }

        void UpdateSphere(Vector3 newPos)
        {
            bSphere.Center = newPos;
        }

        Vector3 CenterPosition
        {
            get
            {
                if (this.GeomType == GeometryType.Sphere)
                    return bSphere.Center;
                if (this.GeomType == GeometryType.Box)
                {
                    Vector3 centerOffset = (bBox.Max - bBox.Min) / 2;
                    return bBox.Min + centerOffset;
                }
                else
                    throw new Exception("Unsupported GeometryType");
            }
        }

        public bool checkCollision(Geometry otherObj, ref Vector3 contactNormal, ref float penetrationDepth)
        {
            
            GeometryType t1 = this.GeomType;
            GeometryType t2 = otherObj.GeomType;

            switch ((byte)t1 * maxType + (byte)t2)
            {
                case ((byte)GeometryType.Box * maxType + (byte)GeometryType.Box):
                    if (bBox.Intersects(otherObj.BBox))
                    {
                        getContactNormalBox(this, otherObj, ref contactNormal, ref penetrationDepth);
                        return true;
                    }

                    return false;
                case ((byte)GeometryType.Box * maxType + (byte)GeometryType.Sphere):
                    if (bBox.Intersects(otherObj.bSphere))
                    {
                        getContactNormalBox(this, otherObj, ref contactNormal, ref penetrationDepth);
                        return true;
                    }

                    return false;
                case ((byte)GeometryType.Sphere * maxType + (byte)GeometryType.Box):
                    if (bSphere.Intersects(otherObj.BBox))
                    {
                        getContactNormalBox(otherObj, this, ref contactNormal, ref penetrationDepth); //still use the box method, but reverse the normal
                        contactNormal *= -1;
                        return true;
                    }
                    return false;
                case ((byte)GeometryType.Sphere * maxType + (byte)GeometryType.Sphere):
                    if (bSphere.Intersects(otherObj.BSphere))
                    {
                        getContactNormalSphere(otherObj, ref contactNormal, ref penetrationDepth);
                        return true;
                    }
                    return false;
                default:
                    throw new Exception("Unsupported Collision Type.");
            }

        }

        //find normal along edge with least penetration
        void getContactNormalBox(Geometry boxObject, Geometry otherObject, ref Vector3 contactNormal, ref float penetrationDepth)
        {

            Vector3 min = boxObject.BBox.Min;
            Vector3 center = boxObject.CenterPosition;
            Vector3 offset = otherObject.CenterPosition - boxObject.CenterPosition;
            Vector3 scaledoffset;
            float scaledPenetrationDepth;
            float otherObjDimension = 0f;
            if (otherObject.GeomType == GeometryType.Box)
                otherObjDimension = (otherObject.CenterPosition - otherObject.BBox.Min).X;

            float halfWidth = center.X - min.X;
            float halfHeight = center.Y - min.Y;
            float halfDepth = center.Z - min.Z;

            if (otherObject.GeomType == GeometryType.Box)
            {
                float otherWidth = otherObject.CenterPosition.X - otherObject.BBox.Min.X;
                float otherHeight = otherObject.CenterPosition.Y - otherObject.BBox.Min.Y;
                float otherDepth = otherObject.CenterPosition.Z - otherObject.BBox.Min.Z;
                scaledoffset.X = offset.X / otherWidth;
                scaledoffset.Y = offset.Y / otherHeight;
                scaledoffset.Z = offset.Z / otherDepth;
            }
            else
                scaledoffset = offset;

            float xPen = Vector3.Dot(scaledoffset, Vector3.UnitX);
            float yPen = Vector3.Dot(scaledoffset, Vector3.UnitY);
            float zPen = Vector3.Dot(scaledoffset, Vector3.UnitZ);


            float unscaledXPen = Vector3.Dot(offset, Vector3.UnitX);
            float unscaledYPen = Vector3.Dot(offset, Vector3.UnitY);
            float unscaledZPen = Vector3.Dot(offset, Vector3.UnitZ);


            float absXPen = Math.Abs(xPen);
            float absYPen = Math.Abs(yPen);
            float absZPen = Math.Abs(zPen);


            if (xPen >= 0)
            {
                contactNormal = Vector3.UnitX;
                penetrationDepth = halfWidth - unscaledXPen;
                scaledPenetrationDepth = halfWidth - xPen;
            }
            else
            {
                contactNormal = -Vector3.UnitX;
                penetrationDepth = halfWidth + unscaledXPen;
                scaledPenetrationDepth = halfWidth + xPen;
            }

            // Now check y axis for smaller penetration
            if (halfHeight - absYPen < scaledPenetrationDepth)
            {
                if (yPen >= 0)
                {
                    contactNormal = Vector3.UnitY;
                    penetrationDepth = halfHeight - unscaledYPen;
                    scaledPenetrationDepth = halfHeight - yPen;
                }
                else
                {
                    contactNormal = -Vector3.UnitY;
                    penetrationDepth = halfHeight + unscaledYPen;
                    scaledPenetrationDepth = halfHeight + yPen;
                }
                if (otherObject.GeomType == GeometryType.Box)
                    otherObjDimension = (otherObject.CenterPosition - otherObject.BBox.Min).Y;
            }

            //and z axis
            if (halfDepth - absZPen < penetrationDepth)
            {
                if (zPen >= 0)
                {
                    contactNormal = Vector3.UnitZ;
                    penetrationDepth = halfDepth - unscaledZPen;
                    scaledPenetrationDepth = halfDepth - zPen;
                }
                else
                {
                    contactNormal = -Vector3.UnitZ;
                    penetrationDepth = halfDepth + unscaledZPen;
                    scaledPenetrationDepth = halfDepth + zPen;
                }
                if (otherObject.GeomType == GeometryType.Box)
                    otherObjDimension = (otherObject.CenterPosition - otherObject.BBox.Min).Z;
            }
            if(otherObject.GeomType == GeometryType.Box)
                penetrationDepth += otherObjDimension;
            else
                penetrationDepth += otherObject.BSphere.Radius;
        }

        void getContactNormalSphere(Geometry otherObject, ref Vector3 contactNormal, ref float penetrationDepth)
        {
            Vector3 offset = otherObject.CenterPosition - this.CenterPosition;
            float dist = offset.Length();
            penetrationDepth = (otherObject.BSphere.Radius + this.BSphere.Radius) - dist;
            offset.Normalize();
            contactNormal = offset;

        }

        BoundingSphere makeBoundingSphereFromModel(Model mod)
        {
            Matrix[] bones = new Matrix[mod.Bones.Count];
            mod.CopyAbsoluteBoneTransformsTo(bones);
            BoundingSphere b = new BoundingSphere();
            bool firstSphereAssigned = false;
            foreach (ModelMesh mesh in mod.Meshes)
            {
                if (!firstSphereAssigned)
                {
                    b = mesh.BoundingSphere;
                    b.Radius *= bones[mesh.ParentBone.Index].Forward.Length(); //absurd. 
                    firstSphereAssigned = true;
                }
                else
                    b = BoundingSphere.CreateMerged(mesh.BoundingSphere, b);
            }
            b.Radius *= rScale;

            return b;

        }

        BoundingBox makeBoundingBoxFromModel(Model mod, Matrix WorldTransform)
        {
            Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
            Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
            Matrix[] bones = new Matrix[mod.Bones.Count];
            mod.CopyAbsoluteBoneTransformsTo(bones);
            Matrix scale = Matrix.CreateScale(wScale, hScale, dScale); 

            foreach (ModelMesh mesh in mod.Meshes)
            {
                //get the transform of the current mesh
                Matrix transform = bones[mesh.ParentBone.Index];

                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    //get the current mesh info
                    int stride = part.VertexBuffer.VertexDeclaration.VertexStride;
                    int vertexBufferSize = part.NumVertices * stride;
                    float[] verticesData = new float[vertexBufferSize / sizeof(float)];
                    part.VertexBuffer.GetData<float>(verticesData);

                    for (int i = 0; i < vertexBufferSize / sizeof(float); i += stride / sizeof(float))
                    {
                        Vector3 transformedPosition = Vector3.Transform(new Vector3(verticesData[i], verticesData[i + 1], verticesData[i + 2]), transform *  scale * WorldTransform);

                        min = Vector3.Min(min, transformedPosition);
                        max = Vector3.Max(max, transformedPosition);


                    }
                }
            }
            return new BoundingBox(min, max);
        }



    }
}
